# -*- coding: utf-8 -*-

# Chroneo -- A Stopwatch and Timer application
#
# Copyright (C) 2010-2012 Valéry Febvre <vfebvre@easter-eggs.com>
# http://code.google.com/p/chroneo/
#
# This file is part of Chroneo.
#
# Chroneo is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Chroneo is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Chroneo -- A Stopwatch and Timer application
"""

import alsaaudio, wave
import os.path, time
import sqlite3
import ecore, elementary

APP_VERSION = '1.0.2'
APP_NAME = 'Chroneo'
DATA_DIR = '/usr/share/chroneo/'
#DATA_DIR = '../data'


#
# Config
#

def dict_factory(cursor, row):
    d = {}
    for idx, col in enumerate(cursor.description):
        d[col[0]] = row[idx]
    return d


class DB(object):
    def __init__(self):
        cfg_path = os.path.join(os.path.expanduser("~"), ".config/chroneo")
        if not os.path.exists(cfg_path):
            os.makedirs(cfg_path)
        db_path = os.path.join(cfg_path, 'chroneo.db')

        self.cnx = sqlite3.connect(db_path)
        self.cnx.row_factory = dict_factory

        cursor = self.cnx.cursor()
        cursor.executescript("""
            CREATE TABLE IF NOT EXISTS settings (
                name  TEXT,
                value TEXT,
                type  TEXT
            );
            """)

        self.save_setting('version', APP_VERSION)

    def get_setting(self, name):
        sql = 'SELECT value, type FROM settings WHERE name = ?'
        row = self.cnx.cursor().execute(sql, (name,)).fetchone()
        if not row:
            return None
        if row['type'] == 'bool':
            return bool(int(row['value']))
        elif row['type'] == 'float':
            return float(row['value'])
        elif row['type'] == 'int':
            return int(row['value'])
        else:
            return row['value']

    def save_setting(self, name, value, data_type = None):
        if self.get_setting(name) is not None:
            sql = "UPDATE settings SET value = ?, type = ? WHERE name = ?"
            self.cnx.cursor().execute(sql, (value, data_type, name))
        else:
            sql = "INSERT INTO settings (name, value, type) VALUES (?, ?, ?)"
            self.cnx.cursor().execute(sql, (name, value, data_type))
        self.cnx.commit()

    def close(self):
        self.cnx.close()

db = DB()


def display_time(sec, precision = 1):
    t = time.strftime('%H:%M:%S', time.gmtime(int(sec)))
    if precision:
        t += '.%%0%dd' % precision % int(((sec - int(sec)) * 10 ** precision))
    return t


#
# Stopwatch
#

class Stopwatch(object):
    def __init__(self, main):
        self.main = main
        self.box = None
        self.timer = None
        self.state = None

    def build(self):
        self.box = elementary.Box(self.main.win)
        self.box.size_hint_align_set(-1, -1)
        self.box.show()

        self.label_time = elementary.Label(self.main.win)
        self.label_time.text_set('00:00:00.0')
        self.label_time.scale_set(4)
        self.label_time.size_hint_weight_set(1, 0)
        self.label_time.size_hint_align_set(.5, 0.5)
        self.box.pack_end(self.label_time)
        self.label_time.show()

        # laps list
        self.list = elementary.List(self.main.win)
        self.list.size_hint_weight_set(1.0, 1.0)
        self.list.size_hint_align_set(-1.0, -1.0)
        self.box.pack_end(self.list)
        self.list.show()
        self.list_selected_item = None

        # buttons
        box_buttons = elementary.Box(self.main.win)
        box_buttons.horizontal_set(True)
        box_buttons.homogenous_set(True)
        box_buttons.size_hint_align_set(-1.0, 0)
        self.box.pack_end(box_buttons)
        box_buttons.show()

        self.button_reset = elementary.Button(self.main.win)
        self.button_reset.text_set('Reset')
        self.button_reset.size_hint_weight_set(1, 0)
        self.button_reset.size_hint_align_set(-1, 0)
        self.button_reset.callback_clicked_add(self.reset)
        box_buttons.pack_end(self.button_reset)
        self.button_reset.show()

        self.button_lap = elementary.Button(self.main.win)
        self.button_lap.text_set('Lap/Split')
        self.button_lap.size_hint_weight_set(1, 0)
        self.button_lap.size_hint_align_set(-1, 0)
        self.button_lap.callback_clicked_add(self.lap_split)
        box_buttons.pack_end(self.button_lap)
        self.button_lap.show()

        self.button_start = elementary.Button(self.main.win)
        self.button_start.text_set('Start')
        self.button_start.size_hint_weight_set(1, 0)
        self.button_start.size_hint_align_set(-1, 0)
        self.button_start.callback_clicked_add(self.start)
        box_buttons.pack_end(self.button_start)
        self.button_start.show()

        self.main.pager.item_simple_push(self.box)

    def lap_split(self, *args):
        t = time.time()
        if self.state in (None, 'stop'):
            return

        sec = t - self.lap_t
        lap = display_time(sec, 3)
        sec = t - self.t
        split = display_time(sec, 3)

        self.lap_t = t
        self.lap_counter += 1

        label_lap = elementary.Label(self.main.win)
        label_lap.text_set('Lap %d' % self.lap_counter)
        label_split = elementary.Label(self.main.win)
        label_split.text_set('Split %d' % self.lap_counter)
        self.list.item_prepend(lap + '  |  ' + split, label_lap, label_split, None, None)
        self.list.go()

    def promote(self):
        if not self.box:
            self.build()
        self.main.pager.item_simple_promote(self.box)

    def start(self, *args):
        t = time.time()
        if self.state in (None, 'stop'):
            if self.state is None:
                self.t = t
                self.lap_t = self.t
                self.lap_counter = 0
            else:
                self.t = t - (self.stop_t - self.t)
                self.lap_t = self.lap_t + (t - self.stop_t)

            #self.timer = ecore.timer_add(.99, self.update)
            self.timer = ecore.timer_add(.099, self.update)

            self.state = 'start'
            self.button_start.text_set('Stop')
            self.main.request_display(True)
        else:
            self.stop(t)

    def stop(self, t = None, reset = False):
        if self.timer:
            self.timer.delete()
            self.timer = None

        self.button_start.text_set('Start')
        self.main.request_display(False)

        if not reset:
            self.state = 'stop'
            self.stop_t = t

    def update(self):
        sec = time.time() - self.t
        self.label_time.text_set(display_time(sec))
        return True

    def reset(self, *args):
        self.stop(reset = True)
        self.list.clear()
        self.label_time.text_set('00:00:00.0')
        self.state = None


#
# Timer
#

class Timer(object):
    def __init__(self, main):
        self.main = main
        self.box = None
        self.timer = None
        self.state = None
        self.ring_counter = 0
        self.ring_timer = None

    def build(self):
        self.box = elementary.Box(self.main.win)
        self.box.show()

        self.clock = elementary.Clock(self.main.win)
        self.clock.edit_set(True)
        self.clock.show_seconds_set(True)
        self.clock.show_am_pm_set(False)
        self.clock.time_set(0, 0, 0)
        self.box.pack_end(self.clock)
        self.clock.show()

        self.label_time = elementary.Label(self.main.win)
        self.label_time.text_set('00:00:00.0')
        self.label_time.scale_set(4)
        self.label_time.size_hint_weight_set(1, 1)
        self.label_time.size_hint_align_set(.5, 0.5)
        self.box.pack_end(self.label_time)
        self.label_time.show()

        # buttons
        box_buttons = elementary.Box(self.main.win)
        box_buttons.horizontal_set(True)
        box_buttons.homogenous_set(True)
        box_buttons.size_hint_align_set(-1.0, 0)
        self.box.pack_end(box_buttons)
        box_buttons.show()

        self.button_reset = elementary.Button(self.main.win)
        self.button_reset.text_set('Reset')
        self.button_reset.size_hint_weight_set(1, 0)
        self.button_reset.size_hint_align_set(-1, 0)
        self.button_reset.callback_clicked_add(self.reset)
        box_buttons.pack_end(self.button_reset)
        self.button_reset.show()

        self.button_start = elementary.Button(self.main.win)
        self.button_start.text_set('Start')
        self.button_start.size_hint_weight_set(1, 0)
        self.button_start.size_hint_align_set(-1, 0)
        self.button_start.callback_clicked_add(self.start)
        box_buttons.pack_end(self.button_start)
        self.button_start.show()

        self.main.pager.item_simple_push(self.box)

    def promote(self):
        if not self.box:
            self.build()
        self.main.pager.item_simple_promote(self.box)

    def reset(self, *args):
        if self.ring_timer:
            self.stop_ring()
            return
        self.stop(reset = True)
        self.label_time.text_set('00:00:00.0')
        self.clock.time_set(0, 0, 0)

    def ring(self):
        self.main.play_sound('alarm.wav')
        self.ring_counter += 1
        if self.ring_counter == 20:
            self.stop_ring()
            return False
        else:
            return True

    def start(self, *args):
        self.stop_ring()
        t = time.time()
        if self.state in (None, 'stop'):
            if self.state is None:
                clock_time = self.clock.time_get()
                self.duration = clock_time[0] * 3600 + clock_time[1] * 60 + clock_time[2]
                if self.duration == 0:
                    return
            self.t = t
            #self.timer = ecore.timer_add(.99, self.update)
            self.timer = ecore.timer_add(.099, self.update)

            self.state = 'start'
            self.button_start.text_set('Stop')
            self.main.request_display(True)
        else:
            self.stop(t)

    def start_ring(self):
        self.ring()
        self.ring_timer = ecore.timer_add(1, self.ring)
        self.button_reset.text_set('Stop Alarm')

    def stop_ring(self, *args):
        if self.ring_timer:
            self.ring_timer.delete()
            self.ring_timer = None
            self.ring_counter = 0
            self.button_reset.text_set('Reset')

    def stop(self, t = None, reset = False):
        if self.timer:
            self.timer.delete()
            self.timer = None

        self.button_start.text_set('Start')
        self.main.request_display(False)

        if not reset:
            self.duration = self.duration - (time.time() - self.t)
            self.state = 'stop'
        else:
            self.state = None

    def update(self):
        sec = self.duration - (time.time() - self.t)
        if sec > 0:
            self.label_time.text_set(display_time(sec))
            return True
        else:
            self.stop(reset = True)
            self.start_ring()


#
# Settings
#

class Settings(object):
    def __init__(self, main):
        self.main = main
        self.box = None
        self.fullscreen = False

    def build(self):
        self.box = elementary.Box(self.main.win)
        self.box.size_hint_align_set(-1, -1)
        self.box.show()

        scroller = elementary.Scroller(self.main.win)
        scroller.bounce_set(False, False)
        scroller.size_hint_weight_set(1.0, 1.0)
        scroller.size_hint_align_set(-1.0, -1.0)
        self.box.pack_end(scroller)
        scroller.show()

        box = elementary.Box(self.main.win)
        box.size_hint_weight_set(1, 0)
        scroller.content_set(box)
        box.show()

        # fullscreen
        toggle = elementary.Check(self.main.win)
        toggle.text_set("Fullscreen")
        toggle.style_set("toggle")
        toggle.size_hint_align_set(-1, 0)
        toggle.state_set(db.get_setting('fullscreen') or False)
        toggle.text_part_set("on", "Yes")
        toggle.text_part_set("off", "No")
        toggle.callback_changed_add(self.toggle_fullscreen)
        box.pack_end(toggle)
        toggle.show()

        # rotate
        toggle = elementary.Check(self.main.win)
        toggle.text_set("Orientation")
        toggle.style_set("toggle")
        toggle.size_hint_align_set(-1, 0)
        toggle.state_set(db.get_setting('rotation') or False)
        toggle.text_part_set("off", "Portrait")          
        toggle.text_part_set("on", "Landscape")
        toggle.callback_changed_add(self.toggle_rotation)
        box.pack_end(toggle)
        toggle.show()

        # sounds
        toggle = elementary.Check(self.main.win)
        toggle.text_set("Sounds")
        toggle.style_set("toggle")
        toggle.size_hint_align_set(-1, 0)
        toggle.state_set(db.get_setting('sounds') or False)
        toggle.text_part_set("on", "Yes")                      
        toggle.text_part_set("off", "No") 
        toggle.callback_changed_add(self.toggle_sounds)
        box.pack_end(toggle)
        toggle.show()

        # volume
        #slider = elementary.Slider(self.main.win)
        #slider.text_set('Volume')
        #slider.size_hint_weight_set(1, 0)
        #slider.size_hint_align_set(-1, 0)
        #slider.indicator_format_set("%3.0f")
        #slider.min_max_set(0, 100)
        #slider.value = float(self.main.mixer.getvolume()[0])
        #box.pack_end(slider)
        #slider.callback_delay_changed_add(self.set_volume)
        #slider.show()

        self.main.pager.item_simple_push(self.box)

    def promote(self):
        if not self.box:
            self.build()
        self.main.pager.item_simple_promote(self.box)

    def set_volume(self, slider):
        self.main.mixer.setvolume(slider.value)

    def toggle_fullscreen(self, toggle):
        fullscreen = toggle.state_get()
        if self.fullscreen != fullscreen:
            self.main.win.fullscreen_set(fullscreen)
            self.fullscreen = fullscreen

    def toggle_rotation(self, toggle):
        db.save_setting('rotation', toggle.state_get(), 'bool')
        self.main.win.rotation_set(toggle.state_get() * 270)

    def toggle_sounds(self, toggle):
        db.save_setting('sounds', toggle.state_get(), 'bool')


#
# About
#

class About(object):
    def __init__(self, main):
        self.main = main
        self.box = None

    def build(self):
        self.box = elementary.Box(self.main.win)
        self.box.size_hint_align_set(-1, -1)
        self.box.show()

        label = elementary.Label(self.main.win)
        label.text_set('<b>%s %s</>' % (APP_NAME, APP_VERSION))
        label.size_hint_align_set(0.5, 0.5)
        self.box.pack_end(label)
        label.show()

        scroller = elementary.Scroller(self.main.win)
        scroller.bounce_set(False, True)
        scroller.size_hint_weight_set(1.0, 1.0)
        scroller.size_hint_align_set(-1.0, -1.0)
        self.box.pack_end(scroller)
        scroller.show()

        box = elementary.Box(self.main.win)
        box.size_hint_weight_set(1, 0)
        scroller.content_set(box)
        box.show()

        about  = """\
<b>Chroneo</> is a Stopwatch and Timer application.

<b>Copyright</> © 2010-2012 Valéry Febvre

<b>Licensed</> under the GNU GPL v3

<b>Homepage</> http://code.google.com/p/chroneo/

If you like this program send me an email to vfebvre@easter-eggs.com\
"""

        entry = elementary.Entry(self.main.win)
        entry.editable_set(False)
        entry.line_wrap_set(True)
        entry.size_hint_align_set(-1, -1)
        entry.entry_set(about.replace('\n', '<br>'))
        box.pack_end(entry)
        entry.show()

        self.main.pager.item_simple_push(self.box)

    def promote(self):
        if not self.box:
            self.build()
        self.main.pager.item_simple_promote(self.box)


class Chroneo(object):
    def __init__(self):
        self.dbus_usage = None
        ecore.idler_add(self.init_dbus_idler)

        self.pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NORMAL)
        self.pcm.setchannels(1)
        self.pcm.setrate(22050)
        self.pcm.setformat(alsaaudio.PCM_FORMAT_U8)
        self.mixer = alsaaudio.Mixer('PCM', cardindex = 0)

        self.win = elementary.Window(APP_NAME, elementary.ELM_WIN_BASIC)
        self.win.title_set(APP_NAME)
        if db.get_setting('rotation') is not None:
            self.win.rotation_set(270 * int(db.get_setting('rotation')))
        self.win.callback_destroy_add(self.quit)

        bg = elementary.Background(self.win)
        self.win.resize_object_add(bg)
        bg.size_hint_weight_set(1, 1)
        bg.show()

        box = elementary.Box(self.win)
        self.win.resize_object_add(box)
        box.size_hint_weight_set(1, 1)
        box.show()

        toolbar = elementary.Toolbar(self.win)
        toolbar.menu_parent_set(self.win)
        toolbar.homogenous_set(False)
        toolbar.size_hint_align_set(-1.0, 0)
        box.pack_end(toolbar)
        toolbar.show()

        self.toolbar_item_stopwatch = toolbar.item_append('stopwatch', "Stopwatch", self.show_stopwatch)
        self.toolbar_item_timer = toolbar.item_append('timer', "Timer", self.show_timer)
        self.toolbar_item_settings = toolbar.item_append('settings', "Settings", self.show_settings)
        self.toolbar_item_about = toolbar.item_append('about', "About", self.show_about)

        self.pager = elementary.Naviframe(self.win)
        self.pager.size_hint_weight_set(1.0, 1.0)
        self.pager.size_hint_align_set(-1.0, -1.0)
        box.pack_end(self.pager)
        self.pager.show()

        self.stopwatch = Stopwatch(self)
        self.timer = Timer(self)
        self.settings = Settings(self)
        self.about = About(self)

        self.show_stopwatch()

        self.win.resize(480, 640)
        self.win.show()

    def init_dbus_idler(self):
        import dbus, e_dbus

        dbus_system = dbus.SystemBus(mainloop = e_dbus.DBusEcoreMainLoop())        
        try:
            self.dbus_usage = dbus.Interface(
                dbus_system.get_object("org.freesmartphone.ousaged", "/org/freesmartphone/Usage"),
                dbus_interface = "org.freesmartphone.Usage"
                )
        except Exception, e:
            print "dbus error: %s" % e

        return False

    def play_sound(self, sound):
        if not db.get_setting('sounds'):
            return

        # without wave
        #f = open(os.path.join(DATA_DIR, 'sounds/%s' % sound), 'rb')
        #self.pcm.write(f.read())
        #f.close()

        f = wave.open(os.path.join(DATA_DIR, 'sounds/%s' % sound), 'rb')
        data = f.readframes(320)
        while data:
            self.pcm.write(data)
            data = f.readframes(320)
        f.close()

    def quit(self, *args):
        self.pcm.close()
        db.close()
        elementary.exit()

    def request_display(self, display):
        if not self.dbus_usage or self.dbus_usage.GetResourceState('Display') == display:
            return
	if display:
            self.dbus_usage.RequestResource('Display')
        else:
            try:
                self.dbus_usage.ReleaseResource('Display')
            except Exception:
                print "Got dbus exception while releasing display resource"

    def show_stopwatch(self, *args):
        self.toolbar_item_stopwatch.selected = True
        self.stopwatch.promote()

    def show_timer(self, *args):
        self.timer.promote()

    def show_settings(self, *args):
        self.settings.promote()

    def show_about(self, *args):
        self.about.promote()


def main():
    elementary.init()
    elementary.theme_overlay_add(os.path.join(DATA_DIR, 'chroneo.edj'))
    Chroneo()
    elementary.run()
    elementary.shutdown()
    return 0

if __name__ == "__main__":
    exit(main())
